if __debug__: # invoke python with -O or -OO to unset
    from SimPy.SimulationTrace import *
else:
    from SimPy.Simulation import *

import random
import math

from SimPy.SimPlot import *

MAX_PACKET_ID = 0
MAX_CONV_ID = 0

class Universe:
  '''A universe to store the packets and communication channels'''
  
  def __init__(self):
        self.persons = []
        self.channels = []
        self.logEntries = []
        self.noops = 0
        self.sent_packets = 0
        self.started_work_on = 0
        self.finished_work_on = 0
        self.work_sum = 0

class Channel:
    '''A communication channel: Meet, Phone, IM, E-Mail'''

    def __init__(self,
                 name,                # Name of the channel (Meet, Phone, IM ..)
                 t_travel,            # Time between sending and comprehending
                 seconds_per_word,    # Seconds reqd to process one word
                 max_packets,         # Max number of packets in inbox
                 expected_rt_mean,    # Mean expected response time (per packet)
                 expected_rt_sdev, # Expected response time std dev (per packet)
                 sim):                # Simulation instance
        self.seconds_per_word = seconds_per_word
        self.max_packets = max_packets
        self.name = name
        self.expected_rt_mean = expected_rt_mean;
        self.expected_rt_sdev = expected_rt_sdev;
        self.sim = sim

    def t_end(self, packet):
        '''Time to compose/comprehend the packet (in seconds)'''
        return packet.length / self.seconds_per_word

    def text(self):
        '''Return string identification of this channel.'''
        return self.name;

class Packet:
    '''Unit of communication'''

    def __init__(self,
                 src_person,          # Person who sends the packet
                 expected_rt,         # Expected response time (in seconds)
                 length,              # Length of the packet (in words)
                 reply_to,            # Reply to which packet? None for new
                 sim):                # Simulation instance
        global MAX_PACKET_ID
        
        self.expected_rt = expected_rt
        self.length = length
        self.sim = sim
        self.src_person = src_person
        self.id = MAX_PACKET_ID
        self.reply_to = reply_to
        MAX_PACKET_ID = MAX_PACKET_ID + 1

    def text(self):
        '''Return string representation of self'''
        return "packet (id = %d) of length %d expected in %d seconds" % (
            self.id,
            self.length,
            self.expected_rt)


class Conversation:
    '''A packet transmission story'''

    def __init__(self,
                 src_person,          # Person P1
                 start_time,          # Time when P1 sends the packet
                 packet,              # The packet
                 channel,             # The channel
                 dest_person,         # Person P2
                 end_time):           # Time when P1 receives reply
        global MAX_CONV_ID
        
        self.src_person = src_person
        self.start_time = start_time
        self.packet = packet
        self.channel = channel
        self.dest_person = dest_person
        self.end_time = end_time
        self.id = MAX_CONV_ID
        MAX_CONV_ID = MAX_CONV_ID + 1

    def response_time(self):
        '''The response time of the conversation (in seconds)'''
        if self.end_time is not None:
            return self.end_time - self.start_time
        else:
            return None

    def text(self):
        '''Return string representation'''
        return "Conversation %d" % self.id


# A person will sleep most of the time, but at random times, will wake up and
# send a packet with a random expected response time and length by a random
# channel (biased towards his preference).
class Person(Process):
    '''A typical person in the company'''
    
    def __init__(self,
                 sleep_time,          # Time between transmissions (in seconds)
                 id,                  # Index of the Person in the company
                 sim,                 # Simulation instance
                 channels_adaptable,  # allow channel priorities to change?
                 bump_list=[], # True/False list for channel bump probability
                 dump_list=[], # True/False list for channel dump probability
                 pref_channel=None): # name of channel if we have a preference
        Process.__init__(self, name = "%d" % id, sim = sim)

        self.id = id
        self.sleep_time = sleep_time
        self.sim = sim
        self.conversations = list()
        self.channel_preference = dict()
        self.working_on = None # Needed for the reporter to report.
        self.state = "Nothing"
        self.channel_adaptability = channels_adaptable;
        self.bump_list = bump_list;
        self.dump_list = dump_list;
        self.working = False
        
        # ASSERT - The ratios are assumed to be only to the tenth in accuracy if
        # this changes then the send packet random chooser should be improved.
        if pref_channel is None:
            for channel in self.sim.univ.channels:
                self.channel_preference[channel] = 0.25;
        elif pref_channel == "Random":
            for channel in self.sim.univ.channels:
                self.channel_preference[channel] = 0.2

            preferred_channel = random.choice(self.sim.univ.channels);
            self.channel_preference[preferred_channel] = 0.4

            self.sim.report("Created %s with preference for %s." % (
                    self.who(),
                    preferred_channel.name))
        else:
            for channel in self.sim.univ.channels:
                if channel.name == pref_channel:
                    self.channel_preference[channel] = 0.4;
                else:
                    self.channel_preference[channel] = 0.2
            self.sim.report("Created %s with preference for %s." % (
                    self.who(),
                    pref_channel))
        
        # Create empty inboxes for every channel
        self.channels = dict()
        self.inbox = dict()
        for channel in self.sim.univ.channels:
            self.channels[channel.name] = channel
            self.inbox[channel] = []

    def who(self):
        '''Return string identification of the person'''

        return self.name

    def is_free(self, channel):
        '''Return true if the person can accept a new packet on this channel'''

        return (len(self.inbox[channel]) < channel.max_packets)
    
    def pick_channel(self):
        '''Returns a channel to pick out a packet from to reply to'''

        # If there is work on the Meet or Phone channels, attend to it
        # immediately, otherwise, attend to work with a uniform probability

        if not self.is_free(self.channels['Meet']):
            return self.channels['Meet']
        elif not self.is_free(self.channels['Phone']):
            return self.channels['Phone']
        elif not self.is_free(self.channels['IM']):
            return random.choice([None, self.channels['IM']])
        elif not self.is_free(self.channels['E-Mail']):
            return random.choice([None, self.channels['E-Mail']])

        return None
    
    def pick_work(self, channel):
        '''Pick a packet to respond to'''

        return random.choice(self.inbox[channel])

    def send_packet(self, to_packet = None, on_channel = None):
        '''Send a new/reply packet on specific/random channel'''

        # Select a person to communicate with
        if to_packet is None:
            # If this is a new conversation packet, select a random person
            persons = []
            for p in self.sim.univ.persons:
                if p.id != self.id:
                    persons.append(p)
            person = random.choice(persons)
            reply_to = None
            packet_type = "new"
        else:
            # If this is a response packet, use the specified person
            person = to_packet.src_person
            reply_to = to_packet
            packet_type = "reply to packet %d" % to_packet.id
        
        # Select a channel to communicate on
        if on_channel is not None:
            channel = on_channel
        else:
            # Less finicky way of doing weighted random choices
            rand_num = random.random();
            for temp_chan in self.channel_preference:
                if self.channel_preference[temp_chan] > rand_num:
                    channel = temp_chan;
                    break;
                else:
                    rand_num -= self.channel_preference[temp_chan];
        
        # Check to see if that person is free on that channel
        # i.e. See if the communication can handle any more packets.
        is_free = person.is_free(channel)

        if channel.name in ['Meet', 'Phone']:
            is_free = is_free and not person.working

        # If the person is free, send the packet to him
        if is_free:
            # Create a packet of random length and expected response time
            length = 1 + random.choice(range(1000))

            # Return time is based on a normal (Gauss) distribution,
            # with added time for packet reading.
            # NOTE: pydoc says random.gauss() faster than random.normalvariate()
            expected_rt  = random.gauss(channel.expected_rt_mean,
                                        channel.expected_rt_sdev);
            expected_rt = expected_rt + (length * channel.seconds_per_word)
            packet = Packet(self, expected_rt, length, reply_to, self.sim)

            # Send it to the selected person
            person.inbox[channel].append(packet)
            
            # If Phone or Meet is used, then interrupt the other person
            if channel.name in ['Meet', 'Phone']:
                self.interrupt(person)
            
            if packet_type == 'new':
                # If starting a new conversation, record it
                conv = Conversation(self,
                                    self.sim.now(),
                                    packet,
                                    channel,
                                    person,
                                    None)
                self.conversations.append(conv)
                self.sim.report("%s started %s." % (self.who(), conv.text()))

            # Print what just happened
            self.sim.report("%s sent %s (%s) to %s via %s." % (self.who(),
                                                               packet.text(),
                                                               packet_type,
                                                               person.who(),
                                                               channel.name))
            #Updating self for reporter and metrics
            self.sim.univ.sent_packets += 1
            self.state = "Sent packet to free person"
        else:
            #Updating self for reporter and metrics
            self.sim.univ.noops += 1
            self.state = "Failed sending packet"

    def close_conversations(self):
        '''Close conversations if any'''
        
        for channel in self.sim.univ.channels:
            for packet in self.inbox[channel]:
                for conv in self.conversations:
                    if conv.packet == packet.reply_to and conv.end_time is None:
                        conv.end_time = self.sim.now()
                        self.sim.report("%s closed %s (rt = %d, ert = %d)." % (
                            self.who(),
                            conv.text(),
                            conv.response_time(),
                            packet.reply_to.expected_rt))

                        # FIXME: Have channel adaptability as a number?
                        # Channels will only move around if we've configured
                        # them to be.  FIXME: if there's only one channel,
                        # redistribute if it's bumped down.
                        if(self.channel_adaptability):
                            # If response time less than expected,
                            # make weighted random choice of bumping
                            if(conv.response_time() <
                               packet.reply_to.expected_rt
                               and random.choice(self.bump_list)):
                                self.channel_preference[channel] += 0.09;
                                self.sim.report("%s bumped %s up" % (
                                    self.who(),
                                    channel.text()));
                                for ch in self.sim.univ.channels:
                                    if ch != channel:
                                        self.channel_preference[ch] -= 0.03;
                                        self.channel_preference[ch] = max(
                                            self.channel_preference[ch],
                                            0)
                                        
                            # If response time is larger than expected,
                            # make weighted random choice of dumping
                            elif(conv.response_time() >
                                 packet.reply_to.expected_rt
                                 and random.choice(self.dump_list)):
                                self.channel_preference[channel] -= 0.09;
                                if self.channel_preference[channel] < 0:
                                    self.channel_preference[channel] = 0.0;
                                self.sim.report("%s dropped %s down" % (
                                    self.who(),
                                    channel.text()));
                                for ch in self.sim.univ.channels:
                                    if ch != channel:
                                        self.channel_preference[ch] += 0.03;

                            # normalize the weights afterwards, they must add up
                            # to 1.
                            weight_total = 0.0;
                            for ch in self.sim.univ.channels:
                                weight_total += self.channel_preference[ch];
                            for ch in self.sim.univ.channels:
                                self.channel_preference[ch] /= weight_total;

    def status(self):
        '''Output status'''
        pref_msg = ""
        resp_msg = ""
        
        packet = self.working_on

        complete_conv = 0.0
        total_resp = dict()

        for channel in self.sim.univ.channels:
            total_resp[channel] = 0
        
        for conv in self.conversations:
            if conv.end_time is not None:
                complete_conv += 1.0
                if conv.response_time > 0:
                    total_resp[conv.channel] += conv.response_time()

        if len(self.conversations) > 0:
            complete_frac = complete_conv / len(self.conversations)
        else:
            complete_frac = 0.0
            
        self.sim.report("%s\t%.2f\t%.2f\t%.2f\t%.2f\t%d\t%d\t%d\t%d\t%.2f" % (
            self.who(),
            self.channel_preference[self.channels['Meet']],
            self.channel_preference[self.channels['Phone']],
            self.channel_preference[self.channels['E-Mail']],
            self.channel_preference[self.channels['IM']],
            total_resp[self.channels['Meet']],
            total_resp[self.channels['Phone']],
            total_resp[self.channels['E-Mail']],
            total_resp[self.channels['IM']],
            complete_frac), True)
            
    def run(self):
        '''Main process function'''

        while True:
            # Sleep for some time or until interrupt
            yield hold, self, random.expovariate(1.0 / self.sleep_time)

            # ------------------------------------------------------------------
            # Assumptions:
            #
            # 1. All conversations are of length 2 (query and response).
            #
            # Logic:
            #
            # 1. Check all inboxes.
            # 2. If either the meet or the phone inbox has packets, pick them,
            #    otherwise, decide whether to send or respond.
            # 3. If send, send a packet.
            # 4. If respond, pick a random packet, spend some time on it, and
            #    optionally send a packet back (conversation continues) or drop
            #    the packet (conversation ends).
            #
            # Conversations:
            #
            # 1. Create a new conversation entry when starting a conversation.
            # 2. When waking up, check for replies that have arrived.
            # 3. If any of them corresponds to a conversation, then close it.
            # ------------------------------------------------------------------
            
            if self.interrupted():
                # I'm interrupted: Am I busy?
                busy = random.choice([True, False, False])
            else:
                # I've woken up to communicate: I'm not busy.
                busy = False

            if not busy:
                # Check to see if any conversation has to be closed
                self.close_conversations()
                
                # Pick a channel to respond on
                # If None, then start a new conversation, else reply
                channel = self.pick_channel()

                # if channel is meet, you're interrupted
                # else, handle it with some probability
                if (channel is self.channels['Meet']
                    or (channel is not None and random.choice([True, False]))):
                    # Pick a random packet
                    packet =  random.choice(self.inbox[channel])
                    
                    # Only reply to new packets (keep conversation lengths at 2)
                    if packet.reply_to is None:
                        #Updating self for reporter
                        self.state = "Started Work"
                        # Work on that packet
                        self.sim.report("%s started working on %s." % (
                            self.who(),
                            packet.text()))

                        working_time = packet.length * channel.seconds_per_word
                        
                        #Updating universe for metrics
                        self.working_on = packet
                        self.sim.univ.started_work_on += 1
                        self.sim.univ.work_sum += working_time
                        
                        self.working = True
                        yield hold, self, working_time
                        self.working = False
                        
                        #Updating universe for metrics
                        self.working_on = None
                        self.sim.univ.finished_work_on += 1
                        
                        self.sim.report("%s finished working on %s." % (
                            self.who(),
                            packet.text()))
                        
                        # Optionally respond
                        respond = random.choice([True, True, True, True, False])
                        if respond:
                            self.send_packet(packet, channel)
                            #Updating self for reporter
                            self.state = "Finished work + Responded"
                        else:
                            #Updating self for reporter
                            self.state = "Finished work"    
                    else:
                        pass
                    #CRITICAL TO BE AT THIS LEVEL.  
                    # Dequeue the packet
                    self.inbox[channel].remove(packet)
                else:
                    #Updating self for reporter
                    self.state = "Tried to send packet"
                    # Start a new conversation
                    self.send_packet()
                    # if busy then why not send an email, that seems to be the
                    # way things work???  FIXME: What does the person do if the
                    # person is busy, work?  Miss a round of processing is what
                    # it is doing now.
                    
            else:
                self.sim.report("%s interrupted, but is busy." % self.who())


class Reporter(Process):
    '''The reporter in the company'''
    
    def __init__(self,
                 sim):
        Process.__init__(self, name = "Reporter", sim = sim)

    def run(self):
        '''Main process function'''

        while True:
            # Sleep for some time (currently 60 minutes)
            yield hold, self, 60*60
            
            avg = 0
            if(self.sim.univ.started_work_on > 0):
                avg = self.sim.univ.work_sum/self.sim.univ.started_work_on
            self.sim.report("%% Begin reporter report Sent:%d Noops: %d Avg Work Size: %d Started: %d Finished: %d. %%" % 
                            (self.sim.univ.sent_packets,
                             self.sim.univ.noops,
                             avg,
                             self.sim.univ.started_work_on,
                             self.sim.univ.finished_work_on))
            for person in self.sim.univ.persons:
                person.status()
            self.sim.report("%%% End reporter report %%%")

class LogEntry:
    def __init__(self, hours, minutes, seconds, text):
        self.seconds = seconds
        self.minutes = minutes
        self.hours = hours
        self.text = text
        
class CollabModel(Simulation):
    '''The collaboration simulation model'''

    def __init__(self, n_persons=10,
                 channel_adaptability=True,
                 bump_list=[True, False, False],
                 dump_list=[True, False, False],
                 max_time=None,
                 preferred_channel=None):
        Simulation.__init__(self)

        self.univ = Universe()
        if max_time is None:
            self.maxTime = 60 * 60 * 8 * 5 * 4 # 4 work weeks (subject to change, of
                                               # course)
        else:
            self.maxTime=max_time;

        self.verbose = False               # False, if only Reporter reports
        self.channel_adaptability=channel_adaptability;
        self.bump_list = bump_list;
        self.dump_list = dump_list;
        self.n_persons = n_persons;
        self.preferred_channel = preferred_channel;

    def report(self, text, bypass = False):
        '''Report with timestamp'''
        secs =  self.now() % 60
        mins = (self.now() / 60) % 60
        hours = (self.now() / 60) / 60
        
        self.univ.logEntries.append(LogEntry(hours, mins, secs, text))
        
        if self.verbose or bypass:
            print "%d\t%02d:%02d:%02d\t%s" % (self.now(),
                                              hours,
                                              mins,
                                              secs,
                                              text)

    def runModel(self):
        '''The starting point of the simulation'''

        # Create the communication channels, now readable.
        self.univ.channels.append(Channel(name='Meet', 
                                          t_travel=300, 
                                          seconds_per_word=0.1, 
                                          max_packets=1, 
                                          expected_rt_mean=600,
                                          expected_rt_sdev=150,
                                          sim=self))
        self.univ.channels.append(Channel(name='Phone', 
                                          t_travel=10,
                                          seconds_per_word=0.25, 
                                          max_packets=1, 
                                          expected_rt_mean=300,
                                          expected_rt_sdev=120,
                                          sim=self))
        self.univ.channels.append(Channel(name='E-Mail', 
                                          t_travel=0, 
                                          seconds_per_word=1, 
                                          max_packets=100, 
                                          expected_rt_mean=7200,
                                          expected_rt_sdev=2400,
                                          sim=self))
        self.univ.channels.append(Channel(name='IM', 
                                          t_travel=0, 
                                          seconds_per_word=1, 
                                          max_packets=10, 
                                          expected_rt_mean=900,
                                          expected_rt_sdev=300,
                                          sim=self))

        self.initialize()

        # Create the persons in the company
        for i in range(self.n_persons):
            self.univ.persons.append(Person(30*60, i, self,
                                            channels_adaptable=self.channel_adaptability,
                                            bump_list=self.bump_list,
                                            dump_list=self.dump_list,
                                            pref_channel=self.preferred_channel));
        
        # Add the persons to run
        for p in self.univ.persons:
            self.activate(p, p.run())
        
        # Add the reporter to run
        reporter = Reporter(self);
        self.activate(reporter, reporter.run())
        
        self.simulate(until = self.maxTime)


if __name__ == '__main__':
    cm = CollabModel(n_persons=10,
                     channel_adaptability=True,
                     bump_list=[True,False],
                     dump_list=[True],
                     preferred_channel=None);
    cm.report("Starting...")
    cm.runModel()
    cm.report("Finished...")
